﻿//#define USE_PARALLEL
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
using VersionComparer.Base;
using VersionComparer.Configuration;
using VersionComparer.DataModel;


namespace VersionComparer.Comparer
{
    public class ComparisonEngine
    {

        #region statics
        static ComparisonEngine current = ServiceLocator.Current.CreateInstance<ComparisonEngine, ComparisonEngine>();
        public static ComparisonEngine Current
        {
            get
            {
                return current;
            }
        }

        #endregion statics

        #region events
        public event EventHandler<ComparisonFinishedEventArgs> ComparisonFinished;
        public event EventHandler<ItemEventArgs> ItemProcessed;
        public event EventHandler<ComparisonStartingEventArgs> ComparisonStarting;
        public event EventHandler<UpdateProgressEventArgs> UpdateProgress;

        #endregion events

        #region fields
        Stopwatch executionTime = new Stopwatch();
        int currentAlgorithm;
        Dictionary<Guid, ComparableItem> itemsCache = new Dictionary<Guid, ComparableItem>();
        ProgressManager progressManager;
        Dictionary<Type, double> typeOrders = new Dictionary<Type, double>();
        #endregion fields

        #region properties
        public bool IsProcessing
        {
            get
            {
                return ThreadManager.Current.IsRunning<ItemNamePairList>(Compare);
            }
        }
        private ComparisonAlgorithm CurrentAlgorithm
        {
            get
            {
                if (currentAlgorithm == (int)ComparisonAlgorithm.Auto)
                {
                    if (executionTime.ElapsedMilliseconds > Settings.Current.FastMilliseconds)
                        CurrentAlgorithm = ComparisonAlgorithm.Fast;
                }
                return (ComparisonAlgorithm)currentAlgorithm;
            }
            set
            {
                Interlocked.Exchange(ref currentAlgorithm, (int)value);
            }
        }
        #endregion properties

        internal void OnUpdateProgress(int step, int maxstep)
        {
            this.UpdateProgress.Execute ( this , new UpdateProgressEventArgs ( step , maxstep ));
        }

        protected ComparableItem CreateFromItemName(ItemId itemId)
        {
            ComparableItem item;
            if (!itemsCache.TryGetValue(itemId.Id, out item))
            {
                lock (itemsCache)
                {
                    if (!itemsCache.TryGetValue(itemId.Id, out item))
                    {
                        item = ComparableItemCreator.Create(itemId);
                        itemsCache.Add(itemId.Id, item);
                    }
                    item.Text = itemId.Name;
                }
            }
            else
                item.Text = itemId.Name;
            return item;
        }
        public void ClearCache()
        {
            ComparableItemCreator.Clear();
        }
        public void RefreshDifferences(ItemNamePairList items, bool cancelRefreshIfProcessing = false)
        {
            if (IsProcessing)
            {
                if (cancelRefreshIfProcessing)
                    CancelComparison();
                return;
            }
            CompareItems(items);
        }
        protected void CompareMatches(ItemMatcher.ItemMatch match)
        {
            CompareItems(match.Root , match.Item1, match.Item2);
        }
        
        private double GetTypeOrder(Type type)
        {
            double order = 0;
            if (!typeOrders.TryGetValue(type, out order))
            {
                lock (typeOrders)
                {
                    if (!typeOrders.TryGetValue(type, out order))
                    {
                        OrderAttribute orderAttribute = type.GetAttribute<OrderAttribute>();
                        if (orderAttribute != null)
                            order = orderAttribute.Order;
                        typeOrders.Add(type, order);
                    }
                }
            }
            return order;
        }
       
        private int CompareByOriginalIndex(ComparableItem item1, ComparableItem item2)
        {
            var relatedItem1 = item1.RelatedItem;
            var relatedItem2 = item2.RelatedItem;
            if (relatedItem1 == null || relatedItem2 == null)
                return 0;

            return item1.RelatedItem.OriginalIndex.CompareTo(item2.RelatedItem.OriginalIndex);
        }
        private void CheckItemOrders(List<ComparableItem> items1)
        {
            List<ComparableItem> sortedItems1 = new  List<ComparableItem> ( items1);
            sortedItems1.Sort ( CompareByOriginalIndex);
            int previousIndex1 = 0;
            int previousIndex2 = 0;

            for (int i = 0; i < sortedItems1.Count; i++)
            {
                var item1 = sortedItems1[i];
                var item2 = item1.RelatedItem;
                if (item1 != null && item1.Difference == Difference.None )
                {
                    if (item1.OriginalIndex < previousIndex1)
                    {
                        item1.Difference = Difference.OutOfOrder;
                        if ( item2 != null)
                            item2.Difference = Difference.OutOfOrder;
                    }
                    if ( item1 != null )
                        previousIndex1 = item1.OriginalIndex;
                    if ( item2 != null)
                        previousIndex2 = item2.OriginalIndex;
                }
            }
        }
        /// <summary>
        /// Processes a root item
        /// </summary>
        /// <param name="item">root item to process</param>
        private void ProcessRootItem(ComparableItem item)
        {
            bool requiresUpdate1 = item.RequiresUpdate;
            bool requiresUpdate2 = item.RelatedItem.RequiresUpdate;
            
            if (requiresUpdate1 || requiresUpdate2 || Settings.Current.SomethingChanged)
            {
                Settings.Current.ResetChanges();
                item.Refresh();
                item.RelatedItem.Refresh();
                CompareItems(item, item, item.RelatedItem);
            }
            ItemProcessed.Execute(this, new ItemEventArgs(item));
        }
        private ComparableItem GetItem(ItemNamePairList list , ItemId itemId)
        {
            if (itemId == null)
                return null;
            ComparableItem result = CreateFromItemName(itemId);
            result.CurrentRules = list.Rules;
            return result; 
        }
        public void Refresh(ComparableItem[] items)
        {
        }
        public void CancelComparison()
        {
            ThreadManager.Current.Stop < ItemNamePairList>(Compare);
        }

        #region comparison algorithm
        private int CompareItemOrder(Type item1, Type item2)
        {
            double order1 = GetTypeOrder(item1);
            double order2 = GetTypeOrder(item2);
            return order1.CompareTo(order2);

        }
        /// <summary>
        /// Compares two items
        /// </summary>
        /// <param name="root">root item (for performance - the root item could be retrieved from the compared items)</param>
        /// <param name="item1">source item to compare</param>
        /// <param name="item2">destination item to compare</param>
        /// <returns></returns>
        protected bool CompareItems(ComparableItem root, ComparableItem item1, ComparableItem item2)
        {
            double totalSize = 0;
            double percentage = item1.Percentage;
            try
            {
                #region check for differences - if differences exist stop the comparison
                int maxErrorCount = Settings.Current.MaxErrorCount;
                if (root.CurrentErrorCount > maxErrorCount)
                {
                    root.Difference = Difference.TooManyErrors;
                    return false;
                }
                item1.RelatedItem = item2;
                item2.RelatedItem = item1;
                Difference difference = item1.DifferenceRelativeTo(item2);
                item1.Difference = difference;
                if (difference != Difference.None && (!item1.IgnoreErrors || !item2.IgnoreErrors))
                    root.IncrementErrorCount();

                switch (difference)
                {
                    case Difference.Unmatched:
                        item2.Difference = Difference.Unexistant;
                        item2.SubItems = new ComparableItem[0];
                        item1.SubItems = new ComparableItem[0];
                        return true;
                    case Difference.Unexistant:
                        item2.Difference = Difference.Unmatched;
                        item2.SubItems = new ComparableItem[0];
                        item1.SubItems = new ComparableItem[0];
                        return true;
                    default:
                        item2.Difference = difference;
                        break;
                }
                #endregion check for differences - if differences exist stop the comparison

                List<Type> sortedKeys = new List<Type>();
                Dictionary<Type, ItemMatcher> itemsLists = new Dictionary<Type, ItemMatcher>();
                var item1Childs = item1.GetChilds();
                var item2Childs = item2.GetChilds();
                int index = 0;
                var algorithm = CurrentAlgorithm;
                #region extract childs by type
                foreach (var child in item1Childs)
                {
                    child.OriginalIndex = index++;

                    string fullText = child.FullText;
                    double childLength = fullText == null ? 0 : fullText.Length;
                    child.Percentage = childLength;
                    totalSize += childLength;
                    Type childType = child.GetType();
                    ItemMatcher listPair;
                    if (!itemsLists.TryGetValue(childType, out listPair))
                    {
                        sortedKeys.Add(childType);
                        itemsLists.Add(childType, listPair = new ItemMatcher(algorithm, root, item1, item2));
                    }
                    listPair.AddSourceItem(child);
                }
                index = 0;
                foreach (var child in item2Childs)
                {
                    child.OriginalIndex = index++;
                    ItemMatcher listPair;
                    Type childType = child.GetType();
                    if (!itemsLists.TryGetValue(childType, out listPair))
                    {
                        itemsLists.Add(childType, listPair = new ItemMatcher(algorithm, root, item1, item2));
                        sortedKeys.Add(childType);
                    }
                    listPair.AddDestinationItem(child);
                }
                #endregion extract childs by type

                totalSize /= percentage;
                foreach (var child in item1Childs)
                    child.Percentage = child.Percentage / totalSize;

                List<ComparableItem> resultChilds1 = new List<ComparableItem>();
                List<ComparableItem> resultChilds2 = new List<ComparableItem>();
                sortedKeys.Sort(CompareItemOrder);
                foreach (Type type in sortedKeys)
                {
                    ItemMatcher listPair = itemsLists[type];
                    listPair.Match();

                    foreach (var match in listPair.Matches)
                    {
                        resultChilds1.Add(match.Item1);
                        resultChilds2.Add(match.Item2);
                    }
                    if (1 == listPair.Matches.Length)
                        CompareMatches(listPair.Matches[0]);
                    else
                    {
#if USE_PARALLEL
                        Parallel.ForEach(listPair.Matches, CompareMatches);
#else
                        foreach (var match in listPair.Matches)
                            CompareMatches(match);
#endif
                    }
                }

                if (!Settings.Current.AllowReordering)
                {
                    CheckItemOrders(resultChilds1);
                }
                item1.SubItems = resultChilds1.ToArray();
                item2.SubItems = resultChilds2.ToArray();

                foreach (var child in resultChilds1)
                    child.ProcessFinished();
                foreach (var child in resultChilds2)
                    child.ProcessFinished();
                return true;
            }
            finally
            {
                if (totalSize == 0)
                    progressManager.ItemCompleted(item1);
            }
        }
        /// <summary>
        /// Executes the comparison algorithm to the list of items
        /// </summary>
        /// <param name="thread">thread manager - for cancelling and progress update</param>
        /// <param name="list">list of items to compare</param>
        private void Compare(ManagedThread thread, ItemNamePairList list)
        {
            if (list == null)
                return;

            List<ComparableItem> items = new List<ComparableItem>();
            try
            {
                OnUpdateProgress(0, 0);

                CurrentAlgorithm = Settings.Current.Algorithm;
                ItemId[] items1 = list.Items1;
                ItemId[] items2 = list.Items2;

                for (int i = 0; i < items1.Length || i < items2.Length; i++)
                {
                    ItemId item1 = null;
                    ItemId item2 = null;
                    if (items1.Length > i)
                        item1 = items1[i];
                    if (items2.Length > i)
                        item2 = items2[i];
                    var comparableItem1 = GetItem(list, item1);

                    comparableItem1.Percentage = 1.0 / Math.Max(items1.Length, items2.Length);
                    var comparableItem2 = GetItem(list, item2);
                    if (comparableItem1 != null || comparableItem2 != null)
                    {
                        comparableItem1.Index = i;
                        comparableItem2.Index = i;
                        comparableItem1.RelatedItem = comparableItem2;
                        comparableItem2.RelatedItem = comparableItem1;
                        items.Add(comparableItem1);
                    }
                }
                this.ComparisonStarting.Execute(this, new ComparisonStartingEventArgs(list, items.ToArray()));
                progressManager = new ProgressManager(this);
#if USE_PARALLEL
                Parallel.ForEach(items, ProcessRootItem);
#else
                foreach (var item in items)
                    ProcessRootItem(item);
#endif
            }
            finally
            {
                ComparisonFinished.Execute(this, new ComparisonFinishedEventArgs(items.ToArray()));
            }
        }

        public void CompareItems(ItemNamePairList list)
        {
            ThreadManager.Current.StartNew(Compare, list);
        }
        #endregion comparison algorithm

    }
}
